﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ClientSide
{
    #region Using

    using System.Net;
    using System.Net.Sockets;
    
    #endregion

    /// <summary>
    /// An asynchronous socket. Create an instance of this class and give 
    /// callback_handler to the instance to handle communication through TCP/IP
    /// </summary>
    public class AsyncSocket
    {
        #region Variables 
        
        /// <summary>
        /// Socket to send and receive data
        /// </summary>
        private Socket m_socClient;

        /// <summary>
        /// References a method to be called when a corresponding asynchronous operation completes
        /// </summary>
        private AsyncCallback m_pfnCallBack;

        /// <summary>
        /// Event handler to transfer connect result 
        /// </summary>
        public EventHandler ConnectHandler { get; set; }

        /// <summary>
        /// Event handler to transfer data playing
        /// </summary>
        public EventHandler PlayingHandler { get; set; }

        public Socket SocClient
        {
            get
            {
                return this.m_socClient;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Convert IP Address string to Socket IP Address
        /// </summary>
        /// <param name="interProAddress"></param>
        /// <returns></returns>
        private IPAddress RetrieveIPAddress(string interProAddress)
        {
            IPAddress listenIP = null;
            if (Common.IsLocalhost(interProAddress))
            {
                Common.Logger.Debug("Listen any on localhost");
                // Listen any
                listenIP = IPAddress.Any;
            }
            else
            {
                if (Common.IsIPAddress(interProAddress))
                {
                    if (Common.IsLoopback(interProAddress))
                    {
                        Common.Logger.Debug("Loopback adapter are configured. Listen any on localhost");
                        // Listen any
                        listenIP = IPAddress.Any;
                    }
                    else
                    {
                        Common.Logger.Debug("Listen on specified IP " + interProAddress);
                        // Listen on specified IP
                        listenIP = IPAddress.Parse(interProAddress);
                    }
                }
                else
                {
                    if (Common.IsCorrectHostName(interProAddress))
                    {
                        Common.Logger.Debug("Listen any on " + interProAddress);
                        // Listen any
                        listenIP = IPAddress.Any;
                    }
                    else
                    {
                        Common.Logger.Error("Invalid host name " + interProAddress);
                    }
                }
            }
            return listenIP;
        }

        /// <summary>
        /// Connect client to server
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <param name="port"></param>
        /// <param name="playerName"></param>
        /// <returns></returns>
        public bool Connect(string ipAddress, string port, string playerName)
        {
            try
            {
                // Create the socket instance...
                this.m_socClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                // Create the end point 
                IPEndPoint ipEnd = new IPEndPoint(RetrieveIPAddress(ipAddress), int.Parse(port));

                // Connect to the remote host...
                this.m_socClient.Connect(ipEnd);

                // Connect successfully, show playgame form
                if (this.m_socClient.Connected)
                {
                    // Listen message from server
                    this.WaitForData();

                    // Notify this client has connected to server
                    object text = "CONNECT:" + playerName;
                    byte[] bytes = Encoding.ASCII.GetBytes(text.ToString());
                    this.m_socClient.Send(bytes);
                    return true;
                }
                return false;
            }
            catch (SocketException se)
            {
                Common.Logger.Error(se.Message);
                this.ConnectHandler(this, new SocketEvent("ERROR:" + se.Message));
                this.m_socClient.Close();
                this.m_socClient = null;
                return false;
            }
        }

        /// <summary>
        /// Disconnect out of server
        /// </summary>
        public void Disconnect(string playerName)
        {
            // Notify to server this client will be disconnected
            NotifyDisconnect(playerName);

            // Disconnect from server
            if (this.m_socClient != null)
            {
                this.m_socClient.Close();
                this.m_socClient = null;
            }
        }

        /// <summary>
        /// Notify to server this client is going to disconnect 
        /// </summary>
        private void NotifyDisconnect(string playerName)
        {
            try
            {
                object text = "DISCONNECT:" + playerName + "";
                byte[] bytes = Encoding.ASCII.GetBytes(text.ToString());
                if (this.m_socClient != null)
                {
                    this.m_socClient.Send(bytes);
                }
            }
            catch (SocketException se)
            {
                this.ConnectHandler(this, new SocketEvent("ERROR:" + se.Message));
            }
        }

        private void WaitForData()
        {
            try
            {
                if (m_pfnCallBack == null)
                {
                    m_pfnCallBack = new AsyncCallback(this.OnDataReceived);
                }
                SocketPacket state = new SocketPacket();
                state.thisSocket = this.m_socClient;
                this.m_socClient.BeginReceive(state.dataBuffer, 0, state.dataBuffer.Length, SocketFlags.None, m_pfnCallBack, state);
            }
            catch (SocketException se)
            {
                Common.Logger.Error(se.Message);
                this.ConnectHandler(this, new SocketEvent("ERROR:" + se.Message));
            }
        }

        public void OnDataReceived(IAsyncResult asyn)
        {
            try
            {
                SocketPacket asyncState = (SocketPacket)asyn.AsyncState;
                int byteCount = asyncState.thisSocket.EndReceive(asyn);
                char[] chars = new char[byteCount + 1];
                Encoding.UTF8.GetDecoder().GetChars(asyncState.dataBuffer, 0, byteCount, chars, 0);
                string str = new string(chars);
                str = str.Remove(str.Length - 1);

                switch (str.ToUpper())
                {
                    case "WAITOPPONENT":
                        this.ConnectHandler(this, new SocketEvent("NOTIFY:WAITOPPONENT"));
                        break;
                    case "STARTGAME":
                        this.ConnectHandler(this, new SocketEvent("NOTIFY:STARTGAME"));
                        break;
                    case "MAXIMUMCONNECTION":
                        this.ConnectHandler(this, new SocketEvent("MAXIMUMCONNECTION"));
                        return;
                    default:
                        if (str.StartsWith("RESULTSET"))
                        {
                            this.PlayingHandler(this, new SocketEvent(str));
                        }
                        break;
                }
                // Continue wait for data
                this.WaitForData();
            }
            catch (ObjectDisposedException)
            {
                Common.Logger.Error("Socket has been closed");
                this.ConnectHandler(this, new SocketEvent("ERROR:Socket has been closed"));
            }
            catch (SocketException se)
            {
                Common.Logger.Error(se.Message);
                this.ConnectHandler(this, new SocketEvent("ERROR:" + se.Message));
            }
        }

        /// <summary>
        /// Send playing result to server
        /// </summary>
        /// <param name="result"></param>
        public void SendPlayingResult(string playerName, string result)
        {
            try
            {
                byte[] bytes = Encoding.ASCII.GetBytes("RESULT:" + playerName + "#" + result);
                if (this.m_socClient != null)
                {
                    this.m_socClient.Send(bytes);
                    // Listen message from server
                    this.WaitForData();
                }
            }
            catch (SocketException se)
            {
                this.PlayingHandler(this, new SocketEvent("ERROR:" + se.Message));
            }
        }

        #endregion
    }
}
